পাইথনের হাইপোথিসিস লাইব্রেরি দিয়ে প্রপার্টি-বেসড টেস্টিং আবিষ্কার করুন। উদাহরণ-ভিত্তিক পরীক্ষার বাইরে গিয়ে এজ কেসগুলি খুঁজুন ও শক্তিশালী, নির্ভরযোগ্য সফটওয়্যার তৈরি করুন।
ইউনিট টেস্টের বাইরে: পাইথনের হাইপোথিসিস ব্যবহার করে প্রপার্টি-বেসড টেস্টিং-এর গভীরে অনুসন্ধান
সফটওয়্যার ডেভেলপমেন্টের জগতে, টেস্টিং হলো মানের ভিত্তি। কয়েক দশক ধরে, প্রভাবশালী দৃষ্টান্তটি হলো উদাহরণ-ভিত্তিক টেস্টিং। আমরা যত্ন সহকারে ইনপুট তৈরি করি, প্রত্যাশিত আউটপুট সংজ্ঞায়িত করি, এবং আমাদের কোড পরিকল্পনা অনুযায়ী কাজ করছে কিনা তা যাচাই করার জন্য অ্যাসারশন লিখি। unittest
এবং pytest
-এর মতো ফ্রেমওয়ার্কগুলিতে পাওয়া এই পদ্ধতিটি শক্তিশালী এবং অপরিহার্য। কিন্তু যদি আমি আপনাকে বলি যে একটি পরিপূরক পদ্ধতি আছে যা এমন বাগগুলি উন্মোচন করতে পারে যা আপনি কখনও খুঁজতেই পারেননি?
স্বাগত প্রপার্টি-বেসড টেস্টিং-এর জগতে, একটি দৃষ্টান্ত যা নির্দিষ্ট উদাহরণ পরীক্ষা করা থেকে আপনার কোডের সাধারণ বৈশিষ্ট্য যাচাই করার দিকে মনোযোগ স্থানান্তরিত করে। এবং পাইথন ইকোসিস্টেমে, এই পদ্ধতির অবিসংবাদিত চ্যাম্পিয়ন হলো হাইপোথিসিস নামক একটি লাইব্রেরি।
এই ব্যাপক নির্দেশিকা আপনাকে হাইপোথিসিস সহ প্রপার্টি-বেসড টেস্টিং-এর একজন সম্পূর্ণ নতুন ব্যবহারকারী থেকে একজন আত্মবিশ্বাসী অনুশীলনকারীতে পরিণত করবে। আমরা মূল ধারণাগুলি অন্বেষণ করব, ব্যবহারিক উদাহরণগুলিতে ডুব দেব, এবং আপনার দৈনন্দিন ডেভেলপমেন্ট ওয়ার্কফ্লোতে এই শক্তিশালী টুলটিকে কীভাবে সংহত করতে হয় তা শিখব যাতে আরও শক্তিশালী, নির্ভরযোগ্য এবং বাগ-প্রতিরোধী সফটওয়্যার তৈরি করা যায়।
প্রপার্টি-বেসড টেস্টিং কী? মননে একটি পরিবর্তন
হাইপোথিসিস বুঝতে, আমাদের প্রথমে প্রপার্টি-বেসড টেস্টিং-এর মৌলিক ধারণাটি বুঝতে হবে। আসুন আমরা এটিকে আমাদের পরিচিত ঐতিহ্যবাহী উদাহরণ-ভিত্তিক টেস্টিং-এর সাথে তুলনা করি।
উদাহরণ-ভিত্তিক টেস্টিং: পরিচিত পথ
কল্পনা করুন আপনি একটি কাস্টম সর্টিং ফাংশন, my_sort()
লিখেছেন। উদাহরণ-ভিত্তিক টেস্টিং-এর ক্ষেত্রে, আপনার চিন্তা প্রক্রিয়াটি হবে:
- "আসুন এটি একটি সাধারণ, সাজানো তালিকা দিয়ে পরীক্ষা করি।" ->
assert my_sort([1, 2, 3]) == [1, 2, 3]
- "বিপরীত ক্রমে সাজানো তালিকা সম্পর্কে কী?" ->
assert my_sort([3, 2, 1]) == [1, 2, 3]
- "একটি খালি তালিকা সম্পর্কে কী?" ->
assert my_sort([]) == []
- "নকল উপাদান সহ একটি তালিকা?" ->
assert my_sort([5, 1, 5, 2]) == [1, 2, 5, 5]
- "এবং নেতিবাচক সংখ্যা সহ একটি তালিকা?" ->
assert my_sort([-1, -5, 0]) == [-5, -1, 0]
এটি কার্যকর, তবে এর একটি মৌলিক সীমাবদ্ধতা রয়েছে: আপনি কেবল সেই কেসগুলি পরীক্ষা করছেন যা আপনি ভাবতে পারেন। আপনার পরীক্ষাগুলি আপনার কল্পনাশক্তির মতোই ভালো। আপনি খুব বড় সংখ্যা, ফ্লোটিং-পয়েন্টের ভুল, নির্দিষ্ট ইউনিকোড অক্ষর, বা ডেটার জটিল সংমিশ্রণ যা অপ্রত্যাশিত আচরণের দিকে নিয়ে যায় এমন এজ কেসগুলি মিস করতে পারেন।
প্রপার্টি-বেসড টেস্টিং: ইনভেরিয়েন্ট নিয়ে চিন্তা করা
প্রপার্টি-বেসড টেস্টিং স্ক্রিপ্টটি উল্টে দেয়। নির্দিষ্ট উদাহরণ দেওয়ার পরিবর্তে, আপনি আপনার ফাংশনের বৈশিষ্ট্যগুলি, বা ইনভেরিয়েন্টগুলি সংজ্ঞায়িত করেন—নিয়মগুলি যা যে কোনও বৈধ ইনপুটের জন্য সত্য হওয়া উচিত। আমাদের my_sort()
ফাংশনের জন্য, এই বৈশিষ্ট্যগুলি হতে পারে:
- আউটপুট সাজানো: সংখ্যার যেকোনো তালিকার জন্য, আউটপুট তালিকার প্রতিটি উপাদান তার পরবর্তীটির চেয়ে কম বা সমান।
- আউটপুটে ইনপুটের মতো একই উপাদান রয়েছে: সাজানো তালিকাটি মূল তালিকার একটি পারমুটেশন মাত্র; কোনো উপাদান যোগ বা হারানো হয় না।
- ফাংশনটি আইডেম্পোটেন্ট: একটি ইতিমধ্যে সাজানো তালিকা সাজানো এটিকে পরিবর্তন করা উচিত নয়। অর্থাৎ,
my_sort(my_sort(some_list)) == my_sort(some_list)
।
এই পদ্ধতির সাহায্যে, আপনি পরীক্ষার ডেটা লিখছেন না। আপনি নিয়ম লিখছেন। তারপর আপনি হাইপোথিসিসের মতো একটি ফ্রেমওয়ার্ককে শত শত বা হাজার হাজার এলোমেলো, বৈচিত্র্যময় এবং প্রায়শই জটিল ইনপুট তৈরি করতে দেন যাতে আপনার বৈশিষ্ট্যগুলি ভুল প্রমাণ করা যায়। যদি এটি এমন একটি ইনপুট খুঁজে পায় যা একটি বৈশিষ্ট্য ভঙ্গ করে, তবে এটি একটি বাগ খুঁজে পেয়েছে।
হাইপোথিসিস-এর পরিচয়: আপনার স্বয়ংক্রিয় টেস্ট ডেটা জেনারেটর
হাইপোথিসিস হলো পাইথনের জন্য প্রিমিয়ার প্রপার্টি-বেসড টেস্টিং লাইব্রেরি। এটি আপনার সংজ্ঞায়িত বৈশিষ্ট্যগুলি গ্রহণ করে এবং সেগুলিকে চ্যালেঞ্জ করার জন্য পরীক্ষার ডেটা তৈরি করার কঠিন কাজটি করে। এটি কেবল একটি এলোমেলো ডেটা জেনারেটর নয়; এটি একটি বুদ্ধিমান এবং শক্তিশালী টুল যা কার্যকরভাবে বাগ খুঁজে বের করার জন্য ডিজাইন করা হয়েছে।
হাইপোথিসিসের মূল বৈশিষ্ট্য
- স্বয়ংক্রিয় টেস্ট কেস জেনারেশন: আপনি আপনার প্রয়োজনীয় ডেটার *আকৃতি* সংজ্ঞায়িত করেন (যেমন, "পূর্ণসংখ্যার একটি তালিকা," "কেবল অক্ষর ধারণকারী একটি স্ট্রিং," "ভবিষ্যতের একটি ডেটাইম"), এবং হাইপোথিসিস সেই আকৃতি অনুযায়ী বিভিন্ন ধরনের উদাহরণ তৈরি করে।
- বুদ্ধিমান সংকোচন (Intelligent Shrinking): এটি জাদুর মতো একটি বৈশিষ্ট্য। যখন হাইপোথিসিস একটি ব্যর্থ পরীক্ষার কেস খুঁজে পায় (যেমন, 50টি জটিল সংখ্যার একটি তালিকা যা আপনার সর্ট ফাংশনকে ক্র্যাশ করে), তখন এটি কেবল সেই বিশাল তালিকাটি রিপোর্ট করে না। এটি বুদ্ধিমানের সাথে এবং স্বয়ংক্রিয়ভাবে ইনপুটকে সরল করে ক্ষুদ্রতম সম্ভাব্য উদাহরণ খুঁজে বের করে যা এখনও ব্যর্থতার কারণ হয়। 50-উপাদানযুক্ত তালিকার পরিবর্তে, এটি রিপোর্ট করতে পারে যে ব্যর্থতা কেবল
[inf, nan]
দিয়ে ঘটে। এটি ডিবাগিংকে অবিশ্বাস্যভাবে দ্রুত এবং কার্যকর করে তোলে। - নির্বিঘ্ন ইন্টিগ্রেশন: হাইপোথিসিস
pytest
এবংunittest
-এর মতো জনপ্রিয় টেস্টিং ফ্রেমওয়ার্কগুলির সাথে পুরোপুরি সংহত হয়। আপনি আপনার কর্মপ্রবাহ পরিবর্তন না করেই আপনার বিদ্যমান উদাহরণ-ভিত্তিক পরীক্ষাগুলির পাশাপাশি প্রপার্টি-ভিত্তিক পরীক্ষাগুলি যোগ করতে পারেন। - কৌশলের সমৃদ্ধ লাইব্রেরি (Rich Library of Strategies): এটি সাধারণ পূর্ণসংখ্যা এবং স্ট্রিং থেকে শুরু করে জটিল, নেস্টেড ডেটা স্ট্রাকচার, টাইমজোন-সচেতন ডেটাইম এবং এমনকি NumPy অ্যারে পর্যন্ত সবকিছু তৈরির জন্য অন্তর্নির্মিত "কৌশল"-এর একটি বিশাল সংগ্রহ নিয়ে আসে।
- স্টেটফুল টেস্টিং: আরও জটিল সিস্টেমের জন্য, হাইপোথিসিস স্টেট ট্রানজিশনে বাগ খুঁজে বের করতে অ্যাকশনগুলির ক্রম পরীক্ষা করতে পারে, যা উদাহরণ-ভিত্তিক টেস্টিংয়ের মাধ্যমে কুখ্যাতভাবে কঠিন।
শুরু করা: আপনার প্রথম হাইপোথিসিস টেস্ট
চলুন কাজ শুরু করি। হাইপোথিসিস বোঝার সবচেয়ে ভালো উপায় হলো এটিকে বাস্তবে দেখা।
ইনস্টলেশন
প্রথমে, আপনাকে হাইপোথিসিস এবং আপনার পছন্দের টেস্ট রানার (আমরা pytest
ব্যবহার করব) ইনস্টল করতে হবে। এটি খুবই সহজ:
pip install pytest hypothesis
একটি সহজ উদাহরণ: একটি পরম মান ফাংশন
আসুন একটি সাধারণ ফাংশন বিবেচনা করি যা একটি সংখ্যার পরম মান গণনা করার কথা। একটি সামান্য ত্রুটিপূর্ণ বাস্তবায়ন এরকম দেখতে পারে:
# in a file named `my_math.py` def custom_abs(x): """পরম মান ফাংশনের একটি কাস্টম বাস্তবায়ন।""" if x < 0: return -x return x
এখন, আসুন একটি টেস্ট ফাইল লিখি, test_my_math.py
। প্রথমে, ঐতিহ্যবাহী pytest
পদ্ধতি:
# test_my_math.py (উদাহরণ-ভিত্তিক) def test_abs_positive(): assert custom_abs(5) == 5 def test_abs_negative(): assert custom_abs(-5) == 5 def test_abs_zero(): assert custom_abs(0) == 0
এই পরীক্ষাগুলি পাস হয়। আমাদের ফাংশন এই উদাহরণগুলির উপর ভিত্তি করে সঠিক দেখাচ্ছে। কিন্তু এখন, হাইপোথিসিস দিয়ে একটি প্রপার্টি-ভিত্তিক পরীক্ষা লিখি। পরম মান ফাংশনের একটি মূল বৈশিষ্ট্য কী? ফলাফল কখনই নেতিবাচক হওয়া উচিত নয়।
# test_my_math.py (হাইপোথিসিস সহ প্রপার্টি-ভিত্তিক) from hypothesis import given from hypothesis import strategies as st from my_math import custom_abs @given(st.integers()) def test_abs_property_is_non_negative(x): """বৈশিষ্ট্য: যেকোনো পূর্ণসংখ্যার পরম মান সর্বদা >= 0।""" assert custom_abs(x) >= 0
আসুন এটিকে বিশ্লেষণ করি:
from hypothesis import given, strategies as st
: আমরা প্রয়োজনীয় উপাদানগুলি আমদানি করি।given
হলো একটি ডেকোরেটর যা একটি নিয়মিত টেস্ট ফাংশনকে একটি প্রপার্টি-ভিত্তিক টেস্টে রূপান্তরিত করে।strategies
হলো সেই মডিউল যেখানে আমরা আমাদের ডেটা জেনারেটরগুলি খুঁজে পাই।@given(st.integers())
: এটি পরীক্ষার মূল অংশ।@given
ডেকোরেটর হাইপোথিসিসকে এই টেস্ট ফাংশনটি একাধিকবার চালানোর নির্দেশ দেয়। প্রতিটি রানের জন্য, এটি প্রদত্ত কৌশল,st.integers()
ব্যবহার করে একটি মান তৈরি করবে এবং এটিকে আমাদের টেস্ট ফাংশনের আর্গুমেন্টx
হিসাবে পাস করবে।assert custom_abs(x) >= 0
: এটি আমাদের বৈশিষ্ট্য। আমরা দাবি করি যে হাইপোথিসিস যে পূর্ণসংখ্যাx
-ই তৈরি করুক না কেন, আমাদের ফাংশনের ফলাফল শূন্যের চেয়ে বড় বা সমান হতে হবে।
যখন আপনি pytest
দিয়ে এটি চালাবেন, তখন এটি সম্ভবত অনেক মানের জন্য পাস হবে। হাইপোথিসিস 0, -1, 1, বড় ধনাত্মক সংখ্যা, বড় ঋণাত্মক সংখ্যা এবং আরও অনেক কিছু চেষ্টা করবে। আমাদের সাধারণ ফাংশন এই সমস্তগুলি সঠিকভাবে পরিচালনা করে। এখন, আসুন অন্য একটি কৌশল চেষ্টা করি দেখি আমরা দুর্বলতা খুঁজে পেতে পারি কিনা।
# আসুন ফ্লোটিং পয়েন্ট সংখ্যা দিয়ে পরীক্ষা করি @given(st.floats()) def test_abs_floats_property(x): assert custom_abs(x) >= 0
যদি আপনি এটি চালান, হাইপোথিসিস দ্রুত একটি ব্যর্থ কেস খুঁজে পাবে!
Falsifying example: test_abs_floats_property(x=nan) ... assert custom_abs(nan) >= 0 AssertionError: assert nan >= 0
হাইপোথিসিস আবিষ্কার করেছে যে আমাদের ফাংশন, যখন float('nan')
(একটি সংখ্যা নয়) দেওয়া হয়, তখন nan
ফেরত দেয়। nan >= 0
এই অ্যাসারশনটি মিথ্যা। আমরা এইমাত্র একটি সূক্ষ্ম বাগ খুঁজে পেয়েছি যা আমরা সম্ভবত ম্যানুয়ালি পরীক্ষা করার কথা ভাবতাম না। আমরা আমাদের ফাংশনটিকে এই কেসটি পরিচালনা করার জন্য ঠিক করতে পারি, সম্ভবত একটি ValueError
উত্থাপন করে বা একটি নির্দিষ্ট মান ফিরিয়ে দিয়ে।
আরও ভালো, যদি বাগটি একটি খুব নির্দিষ্ট ফ্লোটের সাথে হয়? হাইপোথিসিসের শ্রিনকার একটি বড়, জটিল ব্যর্থ সংখ্যা নিয়ে এটিকে সবচেয়ে সহজ সম্ভাব্য সংস্করণে হ্রাস করত যা এখনও বাগটি ট্রিগার করে।
কৌশলের ক্ষমতা: আপনার টেস্ট ডেটা তৈরি করা
কৌশলগুলি হলো হাইপোথিসিসের প্রাণকেন্দ্র। এগুলি ডেটা তৈরির রেসিপি। লাইব্রেরিতে অন্তর্নির্মিত কৌশলগুলির একটি বিশাল অ্যারে রয়েছে, এবং আপনি সেগুলিকে একত্রিত ও কাস্টমাইজ করতে পারেন যাতে আপনি কল্পনা করতে পারেন এমন কার্যত যেকোনো ডেটা স্ট্রাকচার তৈরি করা যায়।
সাধারণ অন্তর্নির্মিত কৌশল
- সংখ্যাসূচক:
st.integers(min_value=0, max_value=1000)
: পূর্ণসংখ্যা তৈরি করে, ঐচ্ছিকভাবে একটি নির্দিষ্ট সীমার মধ্যে।st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
: ফ্লোট তৈরি করে, বিশেষ মানের উপর সূক্ষ্ম-দানাযুক্ত নিয়ন্ত্রণ সহ।st.fractions()
,st.decimals()
- পাঠ্য:
st.text(min_size=1, max_size=50)
: একটি নির্দিষ্ট দৈর্ঘ্যের ইউনিকোড স্ট্রিং তৈরি করে।st.text(alphabet='abcdef0123456789')
: একটি নির্দিষ্ট ক্যারেক্টার সেট থেকে স্ট্রিং তৈরি করে (যেমন, হেক্স কোডের জন্য)।st.characters()
: স্বতন্ত্র অক্ষর তৈরি করে।
- সংগ্রহ:
st.lists(st.integers(), min_size=1)
: তালিকা তৈরি করে যেখানে প্রতিটি উপাদান একটি পূর্ণসংখ্যা। লক্ষ্য করুন কিভাবে আমরা একটি আর্গুমেন্ট হিসাবে আরেকটি কৌশল পাস করি! এটিকে কম্পোজিশন বলা হয়।st.tuples(st.text(), st.booleans())
: একটি নির্দিষ্ট কাঠামো সহ টাপল তৈরি করে।st.sets(st.integers())
st.dictionaries(keys=st.text(), values=st.integers())
: নির্দিষ্ট কী এবং মান প্রকার সহ ডিকশনারি তৈরি করে।
- সময় সংক্রান্ত:
st.dates()
,st.times()
,st.datetimes()
,st.timedeltas()
। এগুলি টাইমজোন-সচেতন করা যেতে পারে।
- বিবিধ:
st.booleans()
:True
বাFalse
তৈরি করে।st.just('constant_value')
: সর্বদা একই একক মান তৈরি করে। জটিল কৌশলগুলি রচনা করার জন্য দরকারী।st.one_of(st.integers(), st.text())
: প্রদত্ত কৌশলগুলির মধ্যে একটি থেকে একটি মান তৈরি করে।st.none()
: শুধুমাত্রNone
তৈরি করে।
কৌশলগুলি একত্রিত করা এবং রূপান্তর করা
হাইপোথিসিসের আসল ক্ষমতা আসে সহজ কৌশলগুলি থেকে জটিল কৌশল তৈরি করার ক্ষমতা থেকে।
.map()
ব্যবহার করে
.map()
পদ্ধতি আপনাকে একটি কৌশল থেকে একটি মান নিতে এবং এটিকে অন্য কিছুতে রূপান্তরিত করতে দেয়। এটি আপনার কাস্টম ক্লাসগুলির অবজেক্ট তৈরি করার জন্য উপযুক্ত।
# একটি সাধারণ ডেটা ক্লাস from dataclasses import dataclass @dataclass class User: user_id: int username: str # User অবজেক্ট তৈরি করার একটি কৌশল user_strategy = st.builds( User, user_id=st.integers(min_value=1), username=st.text(min_size=3, alphabet='abcdefghijklmnopqrstuvwxyz') ) @given(user=user_strategy) def test_user_creation(user): assert isinstance(user, User) assert user.user_id > 0 assert user.username.isalpha()
.filter()
এবং assume()
ব্যবহার করে
কখনও কখনও আপনাকে কিছু তৈরি করা মান প্রত্যাখ্যান করতে হতে পারে। উদাহরণস্বরূপ, আপনার পূর্ণসংখ্যার একটি তালিকা প্রয়োজন হতে পারে যেখানে যোগফল শূন্য নয়। আপনি .filter()
ব্যবহার করতে পারেন:
st.lists(st.integers()).filter(lambda x: sum(x) != 0)
তবে, .filter()
ব্যবহার করা অদক্ষ হতে পারে। যদি শর্তটি প্রায়শই মিথ্যা হয়, তবে হাইপোথিসিস একটি বৈধ উদাহরণ তৈরি করার চেষ্টা করে অনেক সময় ব্যয় করতে পারে। একটি আরও ভালো পদ্ধতি হলো প্রায়শই আপনার টেস্ট ফাংশনের ভিতরে assume()
ব্যবহার করা:
from hypothesis import assume @given(st.lists(st.integers())) def test_something_with_non_zero_sum_list(numbers): assume(sum(numbers) != 0) # ... আপনার পরীক্ষার লজিক এখানে ...
assume()
হাইপোথিসিসকে বলে: "যদি এই শর্ত পূরণ না হয়, তবে এই উদাহরণটি বাতিল করুন এবং একটি নতুন চেষ্টা করুন।" এটি আপনার পরীক্ষার ডেটা সীমাবদ্ধ করার একটি আরও প্রত্যক্ষ এবং প্রায়শই আরও কার্যকর উপায়।
st.composite()
ব্যবহার করে
সত্যিই জটিল ডেটা জেনারেশনের জন্য যেখানে একটি তৈরি করা মান অন্যটির উপর নির্ভর করে, st.composite()
হলো আপনার প্রয়োজনীয় টুল। এটি আপনাকে এমন একটি ফাংশন লিখতে দেয় যা আর্গুমেন্ট হিসাবে একটি বিশেষ draw
ফাংশন নেয়, যা আপনি ধাপে ধাপে অন্যান্য কৌশল থেকে মান টানতে ব্যবহার করতে পারেন।
একটি ক্লাসিক উদাহরণ হলো একটি তালিকা এবং সেই তালিকার মধ্যে একটি বৈধ সূচক তৈরি করা।
@st.composite def list_and_index(draw): # প্রথমে, একটি নন-খালি তালিকা আঁকুন my_list = draw(st.lists(st.integers(), min_size=1)) # তারপর, একটি সূচক আঁকুন যা সেই তালিকার জন্য বৈধ হওয়ার গ্যারান্টিযুক্ত index = draw(st.integers(min_value=0, max_value=len(my_list) - 1)) return (my_list, index) @given(data=list_and_index()) def test_list_access(data): my_list, index = data # এই অ্যাক্সেস নিরাপদ হওয়ার গ্যারান্টিযুক্ত কারণ আমরা কৌশলটি যেভাবে তৈরি করেছি element = my_list[index] assert element is not None # একটি সাধারণ অ্যাসারশন
হাইপোথিসিস বাস্তবে: বাস্তব-বিশ্বের পরিস্থিতি
আসুন এই ধারণাগুলি আরও বাস্তবসম্মত সমস্যাগুলিতে প্রয়োগ করি যা সফটওয়্যার ডেভেলপাররা প্রতিদিন সম্মুখীন হন।
দৃশ্যপট 1: একটি ডেটা সিরিয়ালাইজেশন ফাংশন পরীক্ষা করা
একটি ফাংশন কল্পনা করুন যা একটি ব্যবহারকারীর প্রোফাইল (একটি ডিকশনারি) একটি URL-নিরাপদ স্ট্রিংয়ে সিরিয়ালাইজ করে এবং অন্য একটি ফাংশন যা এটিকে ডিসিরিয়ালাইজ করে। একটি মূল বৈশিষ্ট্য হলো প্রক্রিয়াটি পুরোপুরি বিপরীতমুখী হওয়া উচিত।
import json import base64 def serialize_profile(data: dict) -> str: """একটি ডিকশনারিকে URL-নিরাপদ base64 স্ট্রিংয়ে সিরিয়ালাইজ করে।""" json_string = json.dumps(data) return base64.urlsafe_b64encode(json_string.encode('utf-8')).decode('utf-8') def deserialize_profile(encoded_str: str) -> dict: """একটি স্ট্রিংকে আবার ডিকশনারিতে ডিসিরিয়ালাইজ করে।""" json_string = base64.urlsafe_b64decode(encoded_str.encode('utf-8')).decode('utf-8') return json.loads(json_string) # এখন পরীক্ষার জন্য # আমাদের এমন একটি কৌশল দরকার যা JSON-সামঞ্জস্যপূর্ণ ডিকশনারি তৈরি করে json_dictionaries = st.dictionaries( keys=st.text(), values=st.recursive(st.none() | st.booleans() | st.floats(allow_nan=False) | st.text(), lambda children: st.lists(children) | st.dictionaries(st.text(), children), max_leaves=10) ) @given(profile=json_dictionaries) def test_serialization_roundtrip(profile): """বৈশিষ্ট্য: একটি এনকোডেড প্রোফাইল ডিসিরিয়ালাইজ করলে মূল প্রোফাইলটি ফেরত আসা উচিত।""" encoded = serialize_profile(profile) decoded = deserialize_profile(encoded) assert profile == decoded
এই একক পরীক্ষাটি আমাদের ফাংশনগুলিকে বিশাল বৈচিত্র্যের ডেটা দিয়ে আঘাত করবে: খালি ডিকশনারি, নেস্টেড তালিকা সহ ডিকশনারি, ইউনিকোড অক্ষর সহ ডিকশনারি, অদ্ভুত কী সহ ডিকশনারি এবং আরও অনেক কিছু। এটি কয়েকটি ম্যানুয়াল উদাহরণ লেখার চেয়ে অনেক বেশি পুঙ্খানুপুঙ্খ।
দৃশ্যপট 2: একটি সর্টিং অ্যালগরিদম পরীক্ষা করা
আসুন আমাদের সর্টিং উদাহরণটি আবার দেখি। এখানে আমরা পূর্বে সংজ্ঞায়িত বৈশিষ্ট্যগুলি কীভাবে পরীক্ষা করব তা দেখানো হয়েছে।
from collections import Counter def my_buggy_sort(numbers): # আসুন একটি সূক্ষ্ম বাগ যোগ করি: এটি নকল উপাদান বাদ দেয় return sorted(list(set(numbers))) @given(st.lists(st.integers())) def test_sorting_properties(numbers): sorted_list = my_buggy_sort(numbers) # বৈশিষ্ট্য 1: আউটপুট সাজানো for i in range(len(sorted_list) - 1): assert sorted_list[i] <= sorted_list[i+1] # বৈশিষ্ট্য 2: উপাদানগুলি একই (এটি বাগটি খুঁজে পাবে) assert Counter(numbers) == Counter(sorted_list) # বৈশিষ্ট্য 3: ফাংশনটি আইডেম্পোটেন্ট assert my_buggy_sort(sorted_list) == sorted_list
যখন আপনি এই পরীক্ষাটি চালাবেন, হাইপোথিসিস দ্রুত প্রপার্টি 2-এর জন্য একটি ব্যর্থ উদাহরণ খুঁজে পাবে, যেমন numbers=[0, 0]
। আমাদের ফাংশন [0]
ফেরত দেয়, এবং Counter([0, 0])
, Counter([0])
-এর সমান নয়। শ্রিনকার নিশ্চিত করবে যে ব্যর্থ উদাহরণটি যথাসম্ভব সহজ, যা বাগের কারণ অবিলম্বে স্পষ্ট করে তুলবে।
দৃশ্যপট 3: স্টেটফুল টেস্টিং
অভ্যন্তরীণ অবস্থা সময়ের সাথে পরিবর্তিত হয় এমন অবজেক্টগুলির (যেমন একটি ডেটাবেস সংযোগ, একটি শপিং কার্ট, বা একটি ক্যাশে) জন্য, বাগ খুঁজে বের করা অবিশ্বাস্যভাবে কঠিন হতে পারে। একটি ত্রুটি ট্রিগার করার জন্য অপারেশনগুলির একটি নির্দিষ্ট ক্রম প্রয়োজন হতে পারে। হাইপোথিসিস ঠিক এই উদ্দেশ্যে RuleBasedStateMachine
প্রদান করে।
একটি ইন-মেমরি কী-ভ্যালু স্টোরের জন্য একটি সাধারণ API কল্পনা করুন:
class SimpleKeyValueStore: def __init__(self): self._data = {} def set(self, key, value): self._data[key] = value def get(self, key): return self._data.get(key) def delete(self, key): if key in self._data: del self._data[key] def size(self): return len(self._data)
আমরা এর আচরণ মডেল করতে পারি এবং একটি স্টেট মেশিন দিয়ে পরীক্ষা করতে পারি:
from hypothesis.stateful import RuleBasedStateMachine, rule, Bundle class KeyValueStoreMachine(RuleBasedStateMachine): def __init__(self): super().__init__() self.model = {} self.sut = SimpleKeyValueStore() # Bundle() নিয়মগুলির মধ্যে ডেটা পাস করতে ব্যবহৃত হয় keys = Bundle('keys') @rule(target=keys, key=st.text(), value=st.integers()) def set_key(self, key, value): self.model[key] = value self.sut.set(key, value) return key @rule(key=keys) def delete_key(self, key): del self.model[key] self.sut.delete(key) @rule(key=st.text()) def get_key(self, key): model_val = self.model.get(key) sut_val = self.sut.get(key) assert model_val == sut_val @rule() def check_size(self): assert len(self.model) == self.sut.size() # পরীক্ষা চালানোর জন্য, আপনি কেবল মেশিন এবং unittest.TestCase থেকে সাবক্লাস তৈরি করুন # pytest-এ, আপনি কেবল টেস্টটিকে মেশিন ক্লাসে অ্যাসাইন করতে পারেন TestKeyValueStore = KeyValueStoreMachine.TestCase
হাইপোথিসিস এখন set_key
, delete_key
, get_key
, এবং check_size
অপারেশনগুলির এলোমেলো ক্রম সম্পাদন করবে, নিরলসভাবে এমন একটি ক্রম খুঁজে বের করার চেষ্টা করবে যা একটি অ্যাসারশনকে ব্যর্থ করে। এটি পরীক্ষা করবে যে মুছে ফেলা একটি কী সঠিকভাবে আচরণ করে কিনা, একাধিক সেট এবং মুছে ফেলার পরে আকার সঙ্গতিপূর্ণ কিনা, এবং আরও অনেক পরিস্থিতি যা আপনি ম্যানুয়ালি পরীক্ষা করার কথা নাও ভাবতে পারেন।
সেরা অনুশীলন এবং উন্নত টিপস
- উদাহরণ ডেটাবেস: হাইপোথিসিস স্মার্ট। যখন এটি একটি বাগ খুঁজে পায়, তখন এটি ব্যর্থ উদাহরণটি একটি স্থানীয় ডিরেক্টরিতে (
.hypothesis/
) সংরক্ষণ করে। পরের বার যখন আপনি আপনার পরীক্ষা চালাবেন, তখন এটি প্রথমে সেই ব্যর্থ উদাহরণটি পুনরায় চালাবে, যা আপনাকে তাৎক্ষণিক প্রতিক্রিয়া দেবে যে বাগটি এখনও বিদ্যমান। একবার আপনি এটি ঠিক করলে, উদাহরণটি আর পুনরায় চালানো হয় না। @settings
দিয়ে পরীক্ষা সম্পাদন নিয়ন্ত্রণ করা: আপনি@settings
ডেকোরেটর ব্যবহার করে পরীক্ষা রানের অনেক দিক নিয়ন্ত্রণ করতে পারেন। আপনি উদাহরণের সংখ্যা বাড়াতে পারেন, একটি একক উদাহরণ কতক্ষণ চলতে পারে তার জন্য একটি সময়সীমা নির্ধারণ করতে পারেন (অসীম লুপ ধরার জন্য), এবং কিছু স্বাস্থ্য পরীক্ষা বন্ধ করতে পারেন।@settings(max_examples=500, deadline=1000) # 500টি উদাহরণ চালান, 1-সেকেন্ডের সময়সীমা @given(...) ...
- ব্যর্থতাগুলি পুনরায় তৈরি করা: প্রতিটি হাইপোথিসিস রান একটি সিড ভ্যালু প্রিন্ট করে (যেমন,
@reproduce_failure('version', 'seed')
)। যদি একটি CI সার্ভার এমন একটি বাগ খুঁজে পায় যা আপনি স্থানীয়ভাবে পুনরায় তৈরি করতে পারবেন না, তাহলে আপনি প্রদত্ত সিড সহ এই ডেকোরেটরটি ব্যবহার করে হাইপোথিসিসকে উদাহরণগুলির ঠিক একই ক্রম চালাতে বাধ্য করতে পারেন। - CI/CD এর সাথে ইন্টিগ্রেশন: হাইপোথিসিস যেকোনো কন্টিনিউয়াস ইন্টিগ্রেশন পাইপলাইনের জন্য নিখুঁত। উৎপাদনে পৌঁছানোর আগে অপ্রকাশ্য বাগ খুঁজে বের করার ক্ষমতা এটিকে একটি অমূল্য সুরক্ষা জালে পরিণত করে।
মননে পরিবর্তন: বৈশিষ্ট্য নিয়ে চিন্তা করা
হাইপোথিসিস গ্রহণ করা কেবল একটি নতুন লাইব্রেরি শেখার চেয়েও বেশি কিছু; এটি আপনার কোডের সঠিকতা সম্পর্কে চিন্তা করার একটি নতুন উপায় গ্রহণ করা। "আমার কোন ইনপুটগুলি পরীক্ষা করা উচিত?" জিজ্ঞাসা করার পরিবর্তে, আপনি জিজ্ঞাসা করতে শুরু করেন, "এই কোড সম্পর্কে সর্বজনীন সত্যগুলি কী কী?"
বৈশিষ্ট্যগুলি সনাক্ত করার চেষ্টা করার সময় আপনাকে গাইড করার জন্য এখানে কিছু প্রশ্ন রয়েছে:
- একটি বিপরীত অপারেশন আছে কি? (যেমন, সিরিয়ালাইজ/ডিসিরিয়ালাইজ, এনক্রিপ্ট/ডিক্রিপ্ট, কম্প্রেস/ডিকম্প্রেস)। বৈশিষ্ট্যটি হলো যে অপারেশন এবং এর বিপরীত কার্য সম্পাদন করলে মূল ইনপুটটি ফেরত আসা উচিত।
- অপারেশনটি আইডেম্পোটেন্ট কি? (যেমন,
abs(abs(x)) == abs(x)
)। ফাংশনটি একাধিকবার প্রয়োগ করলে একবার প্রয়োগ করার মতোই একই ফলাফল তৈরি করা উচিত। - একই ফলাফল গণনা করার একটি ভিন্ন, সহজ উপায় আছে কি? আপনি পরীক্ষা করতে পারেন যে আপনার জটিল, অপ্টিমাইজ করা ফাংশনটি একটি সহজ, সুস্পষ্টভাবে সঠিক সংস্করণের মতোই একই আউটপুট তৈরি করে (যেমন, পাইথনের অন্তর্নির্মিত
sorted()
-এর বিরুদ্ধে আপনার উন্নত সর্ট পরীক্ষা করা)। - আউটপুট সম্পর্কে সর্বদা কী সত্য হওয়া উচিত? (যেমন, একটি
find_prime_factors
ফাংশনের আউটপুটে কেবল মৌলিক সংখ্যা থাকা উচিত, এবং তাদের গুণফল ইনপুটের সমান হওয়া উচিত)। - অবস্থা কীভাবে পরিবর্তিত হয়? (স্টেটফুল টেস্টিংয়ের জন্য) যেকোনো বৈধ অপারেশনের পরে কী ইনভেরিয়েন্টগুলি বজায় রাখতে হবে? (যেমন, একটি শপিং কার্টে আইটেমের সংখ্যা কখনই নেতিবাচক হতে পারে না)।
উপসংহার: আত্মবিশ্বাসের এক নতুন স্তর
হাইপোথিসিস সহ প্রপার্টি-বেসড টেস্টিং উদাহরণ-ভিত্তিক টেস্টিংকে প্রতিস্থাপন করে না। জটিল ব্যবসায়িক লজিক এবং সুপরিচিত প্রয়োজনীয়তাগুলির জন্য আপনার এখনও নির্দিষ্ট, হাতে লেখা পরীক্ষার প্রয়োজন (যেমন, "দেশ X এর একজন ব্যবহারকারীকে Y মূল্য দেখতে হবে")।
হাইপোথিসিস যা প্রদান করে তা হলো আপনার কোডের আচরণ অন্বেষণ করার এবং অপ্রত্যাশিত এজ কেসগুলির বিরুদ্ধে রক্ষা করার একটি শক্তিশালী, স্বয়ংক্রিয় উপায়। এটি একটি অক্লান্ত অংশীদার হিসাবে কাজ করে, হাজার হাজার পরীক্ষা তৈরি করে যা যেকোনো মানুষের পক্ষে বাস্তবসম্মতভাবে লেখার চেয়ে বেশি বৈচিত্র্যময় এবং জটিল। আপনার কোডের মৌলিক বৈশিষ্ট্যগুলি সংজ্ঞায়িত করার মাধ্যমে, আপনি একটি শক্তিশালী স্পেসিফিকেশন তৈরি করেন যার বিরুদ্ধে হাইপোথিসিস পরীক্ষা করতে পারে, যা আপনার সফটওয়্যারে আপনার আত্মবিশ্বাসের এক নতুন স্তর প্রদান করে।
পরের বার যখন আপনি একটি ফাংশন লিখবেন, তখন উদাহরণগুলির বাইরে চিন্তা করার জন্য কিছুক্ষণ সময় নিন। নিজেকে জিজ্ঞাসা করুন, "নিয়মগুলি কী? সর্বদা কী সত্য হওয়া উচিত?" তারপর, হাইপোথিসিসকে সেগুলি ভাঙার কঠিন কাজটি করতে দিন। এটি যা খুঁজে পায় তাতে আপনি বিস্মিত হবেন, এবং আপনার কোড এর জন্য আরও ভালো হবে।